﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.CodeDom;
using System.Reflection;
using Microsoft.CSharp;
using System.IO;
using System.CodeDom.Compiler;
using System.Collections;
using System.Reflection.Emit;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq.Expressions;
using Conteinerid.Aspects;

namespace Conteinerid
{
    public class ConteinerId
    {
        public ConteinerId()
        {
            Microsoft.Practices.ObjectBuilder2.Builder a;
            Microsoft.Practices.Unity.ArrayResolutionStrategy b;
            Microsoft.Practices.EnterpriseLibrary.Common.Utility.ConstantStringResolver c;
            Microsoft.Practices.EnterpriseLibrary.Data.ConnectionString d;
            Microsoft.Practices.EnterpriseLibrary.ExceptionHandling.DefaultExceptionHandlingErrorEventArgs e;
            Microsoft.Practices.EnterpriseLibrary.Logging.ContextItems F;
            Microsoft.Practices.EnterpriseLibrary.Security.AndOperator G;
        }
        static CodeExpression Base = new CodeBaseReferenceExpression();
        static CodeExpression This = new CodeThisReferenceExpression();

        IDictionary<Type, Type> mapeamento = new Dictionary<Type, Type>();

        CodeFieldReferenceExpression adviceVisitor;
        CodeFieldReferenceExpression eventos;

        public void Register(Type tipoInterface, Type tipoConcreto)
        {
            mapeamento.Add(tipoInterface, Proxy(tipoConcreto));
        }

        public T New<T>()
        {
            return (T)New(typeof(T));
        }

        public object New(Type @interface)
        {
            try
            {
                var tipo = mapeamento[@interface];

                var parametros = new List<object>();
                var cis = (from c in tipo.GetConstructors() where c.GetCustomAttributes(typeof(DefaultConstructorAttribute), false).Count() == 1 select c);
                var ci = cis.Count() > 0 ? cis.First() : null;
                if (ci != null)
                {
                    var pi = ci.GetParameters();
                    parametros.AddRange(pi.Select(parametro => New(parametro.ParameterType)).Where(p => p != null));
                }
                var o = Activator.CreateInstance(tipo, parametros.ToArray());
                AplicarAspectos(o);
                return o;
            }
            catch (Exception ex)
            {
                return null;
            }
        }

        public object New(Type @interface, params object[] parametros)
        {
            try
            {
                var tipo = mapeamento[@interface];
                var mergedParametros = new List<object>();
                mergedParametros.AddRange(parametros);
                var cis = (from c in tipo.GetConstructors() where c.GetCustomAttributes(typeof(DefaultConstructorAttribute), false).Count() == 1 select c);
                var ci = cis.Count() > 0 ? cis.First() : null;
                if (ci != null)
                {
                    var pi = ci.GetParameters().Where(p => p.GetCustomAttributes(typeof(UserProvidedParameterAttribute), false).Count() == 0).ToArray();
                    mergedParametros.AddRange(pi.Select(parametro => New(parametro.ParameterType)).Where(p => p != null));
                }
                var o = Activator.CreateInstance(tipo, mergedParametros.ToArray());
                return o;
            }
            catch (Exception ex)
            {
                return null;
            }
        }

        public Type Proxy<T>()
        {
            return Proxy(typeof(T));
        }

        public Type Proxy(Type tipo)
        {
            ValidarPossibilidadeDeHeranca(tipo);

            var codeCompileUnit = new CodeCompileUnit();
            var usingNamespace = new CodeNamespace(tipo.Namespace);
            codeCompileUnit.Namespaces.Add(usingNamespace);
            usingNamespace.Imports.Add(new CodeNamespaceImport("Microsoft.Practices.EnterpriseLibrary.ExceptionHandling"));
            usingNamespace.Imports.Add(new CodeNamespaceImport("System.ComponentModel"));

            var proxyType = new CodeTypeDeclaration(string.Format("Proxy_{0}", tipo.GetDescricaoTipo()));

            eventos = proxyType.AddField<EventHandlerList>("eventos", MemberAttributes.Private, typeof(EventHandlerList)).GetReference(This);
            adviceVisitor = proxyType.AddField<AdviceVisitor>("adviceVisitor", MemberAttributes.Private, typeof(EventHandlerList)).GetReference(This);
             
            var baseType = tipo.GetCodeTypeReference();
            usingNamespace.Imports.AddRange(GetNamespaces(tipo));
            proxyType.BaseTypes.Add(baseType);

            usingNamespace.Types.Add(proxyType);

            proxyType.IsClass = true;

            usingNamespace.Imports.AddRange(ConstruirPropriedades(proxyType, tipo.GetProperties()));
            usingNamespace.Imports.AddRange(ConstruirMetodos(proxyType, tipo.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.Default | BindingFlags.InvokeMethod | BindingFlags.NonPublic)));

            GravarFonte(codeCompileUnit);
            return Compilar(codeCompileUnit);
        }

        private static void GravarFonte(CodeCompileUnit codeCompileUnit)
        {
            var provider = new CSharpCodeProvider();
            var sb = new StringBuilder();
            provider.GenerateCodeFromCompileUnit(codeCompileUnit, new StringWriter(sb), new CodeGeneratorOptions());
            var fs = File.CreateText(string.Format(@"C:\Projetos\Estudos\Estudos.Misc\{0}.cs", codeCompileUnit.Namespaces[0].Types[0].Name));
            fs.Write(sb.ToString());
            fs.Close();
        }

        private void AplicarAspectos(object o)
        {

        }

        private CodeNamespaceImport[] GetNamespaces(Type tipo)
        {
            var l = new List<CodeNamespaceImport>();

            foreach (var item in tipo.GetGenericArguments())
            {
                l.Add(new CodeNamespaceImport(item.Namespace));
                l.AddRange(GetNamespaces(item));
            }
            return l.ToArray();
        }

        private static CodeNamespaceImport GetNamespaceFrom(Type type)
        {
            return new CodeNamespaceImport(type.Namespace);
        }

        private CodeNamespaceImport[] ConstruirMetodos(CodeTypeDeclaration proxyType, IEnumerable<MethodInfo> metodos)
        {
            var l = new List<CodeNamespaceImport>();
            foreach (var metodo in metodos.Where(m => !m.IsSpecialName && !m.IsVirtual && m.Name != "Finalize"))
            {
                l.Add(GetNamespaceFrom(metodo.ReturnType));
                l.AddRange(metodo.GetParameters().Select(parameter => GetNamespaceFrom(parameter.ParameterType)));
                proxyType.Members.Add(GetMemberFrom(metodo));
            }
            return l.ToArray();
        }

        private CodeNamespaceImport[] ConstruirPropriedades(CodeTypeDeclaration proxyType, System.Reflection.PropertyInfo[] propriedades)
        {
            IList<CodeNamespaceImport> l = new List<CodeNamespaceImport>();
            foreach (PropertyInfo propriedade in propriedades)
            {
                l.Add(GetNamespaceFrom(propriedade.PropertyType));
                proxyType.Members.Add(GetMemberFrom(propriedade));
            }
            return l.ToArray();
        }

        private CodeTypeMember GetMemberFrom(PropertyInfo propriedade)
        {
            CodeMemberProperty prop = new CodeMemberProperty()
            {
                Name = propriedade.Name,
                Type = propriedade.PropertyType.GetCodeTypeReference()
            };

            //prop.Attributes

            GerarGet(prop);
            GerarSet(prop);
            return prop;
        }

        private void GerarGet(CodeMemberProperty prop)
        {
            CodeTryCatchFinallyStatement tcf = GetTryCatch
                (
                    new CodeMethodReturnStatement(new CodeDefaultValueExpression(prop.Type)),
                    new CodeMethodReturnStatement(new CodePropertyReferenceExpression(Base, prop.Name))
                );
            prop.GetStatements.Add(tcf);
        }

        private void GerarSet(CodeMemberProperty prop)
        {

            CodeAssignStatement assign = new CodeAssignStatement()
            {
                Left = new CodePropertyReferenceExpression()
                {
                    PropertyName = prop.Name,
                    TargetObject = Base
                },
                Right = new CodeVariableReferenceExpression() { VariableName = "value" }
            };
            CodeTryCatchFinallyStatement tcf = GetTryCatch(new CodeMethodReturnStatement(), assign);
            prop.SetStatements.Add(tcf);
        }


        private static CodeTryCatchFinallyStatement GetTryCatch(CodeMethodReturnStatement defaultReturnStatement, params CodeExpression[] statements)
        {
            CodeTryCatchFinallyStatement tcf = new CodeTryCatchFinallyStatement();

            AddTryStatements(statements, tcf);

            CodeCatchClause catchClause = CreateCatchClause(defaultReturnStatement);

            tcf.CatchClauses.Add(catchClause);
            return tcf;
        }

        private static CodeTryCatchFinallyStatement GetTryCatch(CodeMethodReturnStatement defaultReturnStatement, params CodeStatement[] statements)
        {
            CodeTryCatchFinallyStatement tcf = new CodeTryCatchFinallyStatement();

            AddTryStatements(statements, tcf);

            CodeCatchClause catchClause = CreateCatchClause(defaultReturnStatement);

            tcf.CatchClauses.Add(catchClause);
            return tcf;
        }

        private static CodeCatchClause CreateCatchClause(CodeMethodReturnStatement defaultReturnStatement)
        {
            CodeThrowExceptionStatement throwStatement = new CodeThrowExceptionStatement();
            CodeCatchClause catchClause = new CodeCatchClause("ex", typeof(Exception).GetCodeTypeReference());

            CodeConditionStatement ifStatement = new CodeConditionStatement();
            CodeMethodInvokeExpression condicao = new CodeMethodInvokeExpression();
            CodeVariableReferenceExpression argumentoEx = new CodeVariableReferenceExpression("ex");
            CodePrimitiveExpression valor = new CodePrimitiveExpression("Global Policy");

            condicao.Parameters.Add(argumentoEx);
            condicao.Parameters.Add(valor);

            CodeVariableReferenceExpression exceptionPolicy = new CodeVariableReferenceExpression("ExceptionPolicy");
            CodeMethodReferenceExpression handleException = new CodeMethodReferenceExpression(exceptionPolicy, "HandleException");

            condicao.Method = handleException;
            ifStatement.Condition = condicao;

            ifStatement.TrueStatements.Add(throwStatement);

            catchClause.Statements.Add(ifStatement);

            if (defaultReturnStatement != null)
            {
                catchClause.Statements.Add(defaultReturnStatement);
            }
            return catchClause;
        }

        private static void AddTryStatements(CodeExpression[] statements, CodeTryCatchFinallyStatement tcf)
        {
            foreach (var statement in statements)
            {
                tcf.TryStatements.Add(statement);
            }
        }

        private static void AddTryStatements(CodeStatement[] statements, CodeTryCatchFinallyStatement tcf)
        {
            foreach (var statement in statements)
            {
                tcf.TryStatements.Add(statement);
            }
        }

        private CodeTypeMember GetMemberFrom(MethodInfo metodo)
        {
            CodeMemberMethod m = new CodeMemberMethod();

            MemberAttributes attributes = GetAttributes(metodo);
            if (attributes > 0)
            {
                m.Attributes = attributes;
            }


            m.Name = metodo.Name;
            if (metodo.ReturnType != typeof(void))
            {
                m.ReturnType = metodo.ReturnType.GetCodeTypeReference();
            }


            var g = metodo.GetGenericArguments();
            foreach (var item in g)
            {
                m.TypeParameters.Add(item.Name);
            }

            var referenciaMetodo = new CodeMethodReferenceExpression(Base, metodo.Name);
            var chamadaDoMetodo = new CodeMethodInvokeExpression(referenciaMetodo);
            foreach (var item in metodo.GetParameters())
            {
                m.Parameters.Add(GetMemberFrom(item));
                chamadaDoMetodo.Parameters.Add(GetReferenceFrom(item));

                if (item.ParameterType.IsGenericParameter)
                {
                    referenciaMetodo.TypeArguments.Add(item.ParameterType.GetCodeTypeReference());
                }
            }
            Type type = metodo.ReturnType;
            CodeTryCatchFinallyStatement tcf = (type.Name != "Void")
                ? GetTryCatch(NewReturnStatement(m), new CodeMethodReturnStatement() { Expression = chamadaDoMetodo })
                : GetTryCatch(NewReturnStatement(), chamadaDoMetodo);

            m.Statements.Add(tcf);

            return m;
        }

        private MemberAttributes GetAttributes(MethodInfo metodo)
        {
            MemberAttributes memberAttributes = 0;
            if (metodo.IsVirtual)
            {
                memberAttributes |= MemberAttributes.Override;
            }

            if (metodo.IsFamilyAndAssembly)
            {
                memberAttributes |= MemberAttributes.FamilyAndAssembly;
            }

            if (metodo.IsFamilyOrAssembly)
            {
                memberAttributes |= MemberAttributes.FamilyOrAssembly;
            }

            if (metodo.IsAssembly)
            {
                memberAttributes |= MemberAttributes.Assembly;
            }

            if (metodo.IsPublic)
            {
                memberAttributes |= MemberAttributes.Public;
            }

            if (metodo.IsFamily)
            {
                memberAttributes |= MemberAttributes.Family;
            }

            return memberAttributes;
        }

        private CodeExpression GetReferenceFrom(ParameterInfo item)
        {
            return new CodeVariableReferenceExpression(item.Name);
        }

        private CodeParameterDeclarationExpression GetMemberFrom(ParameterInfo parameter)
        {
            return new CodeParameterDeclarationExpression(parameter.ParameterType.GetCodeTypeReference(), parameter.Name);
        }

        private static CodeMethodReturnStatement NewReturnStatement(CodeMemberMethod m)
        {
            return new CodeMethodReturnStatement(new CodeDefaultValueExpression(m.ReturnType));
        }

        private static CodeMethodReturnStatement NewReturnStatement()
        {
            return new CodeMethodReturnStatement();
        }

        private Type Compilar(CodeCompileUnit p)
        {
            CSharpCodeProvider csCodeProvider = new CSharpCodeProvider();

            List<string> assemblyReferences = (from a in Assembly.GetExecutingAssembly().GetReferencedAssemblies() where !a.Name.Contains("Core") select a.Name + ".dll").ToList();
            assemblyReferences.Add(Assembly.GetExecutingAssembly().CodeBase.Substring(Assembly.GetExecutingAssembly().CodeBase.LastIndexOf("/") + 1));

            var compilerParameters = new CompilerParameters(assemblyReferences.ToArray(), string.Format("{0}.dll", DateTime.Now.ToBinary()));
            compilerParameters.GenerateExecutable = false;
            compilerParameters.IncludeDebugInformation = false;
            compilerParameters.GenerateInMemory = false;
            compilerParameters.WarningLevel = 3;
            compilerParameters.TreatWarningsAsErrors = false;
            compilerParameters.CompilerOptions = "/optimize";
            compilerParameters.TempFiles = new TempFileCollection(".", true);
            //compilerParameters.op
            //v3.5
            CompilerResults cr = csCodeProvider.CompileAssemblyFromDom(compilerParameters, p);

            if (cr.Errors.Count > 0)
            {
                var throws = false;
                StringBuilder sb = new StringBuilder();
                var fs = File.CreateText(string.Format(@"C:\Projetos\Estudos\Estudos.Misc\{0}.log", p.Namespaces[0].Types[0].Name));
                foreach (CompilerError item in cr.Errors.OfType<CompilerError>().Where(e => !e.IsWarning))
                {
                    sb.AppendFormat("{0} - {1}\n", item.Line, item.ErrorText);
                    throws = true;
                }
                fs.Write(sb.ToString());
                fs.Close();
                if (throws) throw new Exception(sb.ToString());
            }

            Assembly assembly = Assembly.LoadFrom(cr.PathToAssembly);
            AppDomain.CurrentDomain.DefineDynamicAssembly(assembly.GetName(), AssemblyBuilderAccess.RunAndSave, assembly.CodeBase);
            return assembly.GetTypes()[0];
        }

        private void ValidarPossibilidadeDeHeranca(Type tipo)
        {
            if (tipo.IsSealed)
            {
                throw new ArgumentException("Não é possível criar um proxy para um tipo Fechado para herança(Sealed)!");
            }

            if (tipo.IsInterface)
            {
                throw new ArgumentException("Não é possível criar um proxy para uma interface. Informe um tipo concreto e aberto para herança");
            }

        }

        public void AddAdvice(LogAdvice logAdvice)
        {
            throw new NotImplementedException();
        }
    }
}
